
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <title>【Prometheus】Prometheus 集群与高可用</title>
            </head>
            <body>
            <a href="https://andyoung.blog.csdn.net">原作者博客</a>
            <div id="content_views" class="markdown_views prism-atom-one-light">
                    <svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
                        <path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path>
                    </svg>
                    <p>Prometheus内置了一个基于本地存储的时间序列数据库。在Prometheus设计上，使用本地存储可以降低Prometheus部署和管理的复杂度同时减少高可用（HA）带来的复杂性。 在默认情况下，用户只需要部署多套Prometheus，采集相同的Targets即可实现基本的HA。同时由于Promethus高效的数据处理能力，单个Prometheus Server基本上能够应对大部分用户监控规模的需求。</p> 
<p>当然本地存储也带来了一些不好的地方，首先就是数据持久化的问题，特别是在像Kubernetes这样的动态集群环境下，如果Promthues的实例被重新调度，那所有历史监控数据都会丢失。 其次本地存储也意味着Prometheus不适合保存大量历史数据(一般Prometheus推荐只保留几周或者几个月的数据)。最后本地存储也导致Prometheus无法进行弹性扩展。为了适应这方面的需求，Prometheus提供了remote_write和remote_read的特性，支持将数据存储到远端和从远端读取数据。通过将监控与数据分离，Prometheus能够更好地进行弹性扩展。</p> 
<p>除了本地存储方面的问题，由于Prometheus基于Pull模型，当有大量的Target需要采样本时，单一Prometheus实例在数据抓取时可能会出现一些性能问题，联邦集群的特性可以让Prometheus将样本采集任务划分到不同的Prometheus实例中，并且通过一个统一的中心节点进行聚合，从而可以使Prometheuse可以根据规模进行扩展。</p> 
<p>除了讨论Prometheus自身的高可用，Alertmanager作为Promthues体系中的告警处理中心，本章的最后部分会讨论如何实现Alertmanager的高可用部署。</p> 
<h2><a id="_8"></a><strong>本地存储</strong></h2> 
<p>Prometheus 2.x 采用自定义的存储格式将样本数据保存在本地磁盘当中。如下所示，按照两个小时为一个时间窗口，将两小时内产生的数据存储在一个块(Block)中，每一个块中包含该时间窗口内的所有样本数据(chunks)，元数据文件(meta.json)以及索引文件(index)。</p> 
<pre><code>t0            t1             t2             now
 ┌───────────┐  ┌───────────┐  ┌───────────┐
 │           │  │           │  │           │                 ┌────────────┐
 │           │  │           │  │  mutable  │ &lt;─── write ──── ┤ Prometheus │
 │           │  │           │  │           │                 └────────────┘
 └───────────┘  └───────────┘  └───────────┘                        ^
       └──────────────┴───────┬──────┘                              │
                              │                                   query
                              │                                     │
                            merge ──────────────────────────────────┘
</code></pre> 
<p>当前时间窗口内正在收集的样本数据，Prometheus则会直接将数据保存在内存当中。为了确保此期间如果Prometheus发生崩溃或者重启时能够恢复数据，Prometheus启动时会从写入日志(WAL)进行重播，从而恢复数据。此期间如果通过API删除时间序列，删除记录也会保存在单独的逻辑文件当中(tombstone)。</p> 
<p>在文件系统中这些块保存在单独的目录当中，Prometheus保存块数据的目录结构如下所示：</p> 
<pre><code>./data 
   |- 01BKGV7JBM69T2G1BGBGM6KB12 # 块
      |- meta.json  # 元数据
      |- wal        # 写入日志
        |- 000002
        |- 000001
   |- 01BKGTZQ1SYQJTR4PB43C8PD98  # 块
      |- meta.json  #元数据
      |- index   # 索引文件
      |- chunks  # 样本数据
        |- 000001
      |- tombstones # 逻辑数据
   |- 01BKGTZQ1HHWHV8FBJXW1Y3W0K
      |- meta.json
      |- wal
        |-000001
</code></pre> 
<p>通过时间窗口的形式保存所有的样本数据，可以明显提高Prometheus的查询效率，当查询一段时间范围内的所有样本数据时，只需要简单的从落在该范围内的块中查询数据即可。</p> 
<p>同时该存储方式可以简化历史数据的删除逻辑。只要一个块的时间范围落在了配置的保留范围之外，直接丢弃该块即可。</p> 
<pre><code>                      |
 ┌────────────┐  ┌────┼─────┐  ┌───────────┐  ┌───────────┐  
 │ 1          │  │ 2  |     │  │ 3         │  │ 4         │ . . .
 └────────────┘  └────┼─────┘  └───────────┘  └───────────┘  
                      |
                      |
             retention boundary
</code></pre> 
<h3><a id="_62"></a>本地存储配置</h3> 
<p>用户可以通过命令行启动参数的方式修改本地存储的配置。</p> 
<table><thead><tr><th>启动参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td>–storage.tsdb.path</td><td>data/</td><td>Base path for metrics storage</td></tr><tr><td>–storage.tsdb.retention</td><td>15d</td><td>How long to retain samples in the storage</td></tr><tr><td>–storage.tsdb.min-block-duration</td><td>2h</td><td>The timestamp range of head blocks after which they get persisted</td></tr><tr><td>–storage.tsdb.max-block-duration</td><td>36h</td><td>The maximum timestamp range of compacted blocks,<br>It’s the minimum duration of any persisted block.</td></tr><tr><td>–storage.tsdb.no-lockfile</td><td>false</td><td>Do not create lockfile in data directory</td></tr></tbody></table> 
<p>在一般情况下，Prometheus中存储的每一个样本大概占用1-2字节大小。如果需要对Prometheus Server的本地磁盘空间做容量规划时，可以通过以下公式计算：</p> 
<pre><code>needed_disk_space = retention_time_seconds * ingested_samples_per_second * bytes_per_sample
</code></pre> 
<p>从上面公式中可以看出在保留时间(retention_time_seconds)和样本大小(bytes_per_sample)不变的情况下，如果想减少本地磁盘的容量需求，只能通过减少每秒获取样本数(ingested_samples_per_second)的方式。因此有两种手段，一是减少时间序列的数量，二是增加采集样本的时间间隔。考虑到Prometheus会对时间序列进行压缩效率，减少时间序列的数量效果更明显。</p> 
<h3><a id="_82"></a>从失败中恢复</h3> 
<p>如果本地存储由于某些原因出现了错误，最直接的方式就是停止Prometheus并且删除data目录中的所有记录。当然也可以尝试删除那些发生错误的块目录，不过相应的用户会丢失该块中保存的大概两个小时的监控记录。</p> 
<h2><a id="_86"></a><strong>远程存储</strong></h2> 
<p>Prometheus的本地存储设计可以减少其自身运维和管理的复杂度，同时能够满足大部分用户监控规模的需求。但是本地存储也意味着Prometheus无法持久化数据，无法存储大量历史数据，同时也无法灵活扩展和迁移。</p> 
<p>为了保持Prometheus的简单性，Prometheus并没有尝试在自身中解决以上问题，而是通过定义两个标准接口(remote_write/remote_read)，让用户可以基于这两个接口对接将数据保存到任意第三方的存储服务中，这种方式在Promthues中称为Remote Storage。</p> 
<h3><a id="Remote_Write_92"></a>Remote Write</h3> 
<p>用户可以在Prometheus配置文件中指定Remote Write(远程写)的URL地址，一旦设置了该配置项，Prometheus将采集到的样本数据通过HTTP的形式发送给适配器(Adaptor)。而用户则可以在适配器中对接外部任意的服务。外部服务可以是真正的存储系统，公有云的存储服务，也可以是消息队列等任意形式。</p> 
<p><img src="https://i-blog.csdnimg.cn/blog_migrate/5a78bb681fd9d63cc91e93eda4a868bf.png#pic_center" alt="Remote Write"></p> 
<h3><a id="Remote_Read_101"></a>Remote Read</h3> 
<p>如下图所示，Promthues的Remote Read(远程读)也通过了一个适配器实现。在远程读的流程当中，当用户发起查询请求后，Promthues将向remote_read中配置的URL发起查询请求(matchers,ranges)，Adaptor根据请求条件从第三方存储服务中获取响应的数据。同时将数据转换为Promthues的原始样本数据返回给Prometheus Server。</p> 
<p>当获取到样本数据后，Promthues在本地使用PromQL对样本数据进行二次处理。</p> 
<blockquote> 
 <p>注意：启用远程读设置后，只在数据查询时有效，对于规则文件的处理，以及Metadata API的处理都只基于Prometheus本地存储完成。<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/6975f5f0ef2745605ea4ed86f01f0476.png#pic_center" alt="Remote Read"></p> 
</blockquote> 
<h3><a id="_113"></a>配置文件</h3> 
<p>Prometheus配置文件中添加remote_write和remote_read配置，其中url用于指定远程读/写的HTTP服务地址。如果该URL启动了认证则可以通过basic_auth进行安全认证配置。对于https的支持需要设定tls_concig。proxy_url主要用于Prometheus无法直接访问适配器服务的情况下。</p> 
<p>remote_write和remote_write具体配置如下所示：</p> 
<pre><code>remote_write:
    url: &lt;string&gt;
    [ remote_timeout: &lt;duration&gt; | default = 30s ]
    write_relabel_configs:
    [ - &lt;relabel_config&gt; ... ]
    basic_auth:
    [ username: &lt;string&gt; ]
    [ password: &lt;string&gt; ]
    [ bearer_token: &lt;string&gt; ]
    [ bearer_token_file: /path/to/bearer/token/file ]
    tls_config:
    [ &lt;tls_config&gt; ]
    [ proxy_url: &lt;string&gt; ]

remote_read:
    url: &lt;string&gt;
    required_matchers:
    [ &lt;labelname&gt;: &lt;labelvalue&gt; ... ]
    [ remote_timeout: &lt;duration&gt; | default = 30s ]
    [ read_recent: &lt;boolean&gt; | default = false ]
    basic_auth:
    [ username: &lt;string&gt; ]
    [ password: &lt;string&gt; ]
    [ bearer_token: &lt;string&gt; ]
    [ bearer_token_file: /path/to/bearer/token/file ]
    [ &lt;tls_config&gt; ]
    [ proxy_url: &lt;string&gt; ]
</code></pre> 
<h3><a id="Remote_Storage_Adaptor_149"></a>自定义Remote Storage Adaptor</h3> 
<p>实现自定义Remote Storage需要用户分别创建用于支持remote_read和remote_write的HTTP服务。</p> 
<p>实现自定义Remote Storage需要用户分别创建用于支持remote_read和remote_write的HTTP服务。</p> 
<p><img src="https://i-blog.csdnimg.cn/blog_migrate/887c03fcb1848351756deefddffdb636.png#pic_center" alt="Remote Storage"></p> 
<h3><a id="InfluxdbRemote_Storage_158"></a>使用Influxdb作为Remote Storage</h3> 
<p>目前Prometheus社区也提供了部分对于第三方数据库的Remote Storage支持：</p> 
<table><thead><tr><th>存储服务</th><th>支持模式</th></tr></thead><tbody><tr><td>AppOptics</td><td>write</td></tr><tr><td>Chronix</td><td>write</td></tr><tr><td>Cortex</td><td>read/write</td></tr><tr><td>CrateDB</td><td>read/write</td></tr><tr><td>Gnocchi</td><td>write</td></tr><tr><td>Graphite</td><td>write</td></tr><tr><td>InfluxDB</td><td>read/write</td></tr><tr><td>OpenTSDB</td><td>write</td></tr><tr><td>PostgreSQL/TimescaleDB</td><td>read/write</td></tr><tr><td>SignalFx</td><td>write</td></tr></tbody></table> 
<p>这里将介绍如何使用Influxdb作为Prometheus的Remote Storage，从而确保当Prometheus发生宕机或者重启之后能够从Influxdb中恢复和获取历史数据。</p> 
<p>这里使用docker-compose定义并启动Influxdb数据库服务，docker-compose.yml定义如下：</p> 
<pre><code>version: '2'
services:
  influxdb:
    image: influxdb:1.3.5
    command: -config /etc/influxdb/influxdb.conf
    ports:
      - "8086:8086"
    environment:
      - INFLUXDB_DB=prometheus
      - INFLUXDB_ADMIN_ENABLED=true
      - INFLUXDB_ADMIN_USER=admin
      - INFLUXDB_ADMIN_PASSWORD=admin
      - INFLUXDB_USER=prom
      - INFLUXDB_USER_PASSWORD=prom
</code></pre> 
<p>启动influxdb服务</p> 
<pre><code>$ docker-compose up -d
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
795d0ead87a1        influxdb:1.3.5      "/entrypoint.sh -c..."   3 hours ago         Up 3 hours          0.0.0.0:8086-&gt;8086/tcp   localhost_influxdb_1
</code></pre> 
<p>获取并启动Prometheus提供的Remote Storage Adapter：</p> 
<p><code>go get github.com/prometheus/prometheus/documentation/examples/remote_storage/remote_storage_adapter</code></p> 
<p>获取remote_storage_adapter源码后，go会自动把相关的源码编译成可执行文件，并且保存在$GOPATH/bin/目录下。</p> 
<p>启动remote_storage_adapter并且设置Influxdb相关的认证信息：</p> 
<pre><code>INFLUXDB_PW=prom $GOPATH/bin/remote_storage_adapter -influxdb-url=http://localhost:8086 -influxdb.username=prom -influxdb.database=prometheus -influxdb.retention-policy=autogen
</code></pre> 
<p>修改prometheus.yml添加Remote Storage相关的配置内容：</p> 
<pre><code>remote_write:
  - url: "http://localhost:9201/write"

remote_read:
  - url: "http://localhost:9201/read"
</code></pre> 
<p>重新启动Prometheus能够获取数据后，登录到influxdb容器，并验证数据写入。如下所示，当数据能够正常写入Influxdb后可以看到Prometheus相关的指标。</p> 
<pre><code>docker exec -it 795d0ead87a1 influx
Connected to http://localhost:8086 version 1.3.5
InfluxDB shell version: 1.3.5
&gt; auth
username: prom
password:

&gt; use prometheus
&gt; SHOW MEASUREMENTS
name: measurements
name
----
go_gc_duration_seconds
go_gc_duration_seconds_count
go_gc_duration_seconds_sum
go_goroutines
go_info
go_memstats_alloc_bytes
go_memstats_alloc_bytes_total
go_memstats_buck_hash_sys_bytes
go_memstats_frees_total
go_memstats_gc_cpu_fraction
go_memstats_gc_sys_bytes
go_memstats_heap_alloc_bytes
go_memstats_heap_idle_bytes
</code></pre> 
<p>当数据写入成功后，停止Prometheus服务。同时删除Prometheus的data目录，模拟Promthues数据丢失的情况后重启Prometheus。打开Prometheus UI如果配置正常，Prometheus可以正常查询到本地存储以删除的历史数据记录。</p> 
<p><img src="https://i-blog.csdnimg.cn/blog_migrate/9f64942bbbb0c2763002633da9ed8ede.png#pic_center" alt="从Remote Storage获取历史数据"></p> 
<h2><a id="_264"></a><strong>联邦集群</strong></h2> 
<p>通过Remote Storage可以分离监控样本采集和数据存储，解决Prometheus的持久化问题。这一部分会重点讨论如何利用联邦集群特性对Promthues进行扩展，以适应不同监控规模的变化。</p> 
<h3><a id="_268"></a>使用联邦集群</h3> 
<p>对于大部分监控规模而言，我们只需要在每一个数据中心(例如：EC2可用区，Kubernetes集群)安装一个Prometheus Server实例，就可以在各个数据中心处理上千规模的集群。同时将Prometheus Server部署到不同的数据中心可以避免网络配置的复杂性。</p> 
<p><img src="https://i-blog.csdnimg.cn/blog_migrate/59f0742d9b48e0c3cb67ef3017b80fd9.png#pic_center" alt="联邦集群"></p> 
<p>如上图所示，在每个数据中心部署单独的Prometheus Server，用于采集当前数据中心监控数据。并由一个中心的Prometheus Server负责聚合多个数据中心的监控数据。这一特性在Promthues中称为联邦集群。</p> 
<p>联邦集群的核心在于每一个Prometheus Server都包含一个用于获取当前实例中监控样本的接口/federate。对于中心Prometheus Server而言，无论是从其他的Prometheus实例还是Exporter实例中获取数据实际上并没有任何差异。</p> 
<pre><code>scrape_configs:
  - job_name: 'federate'
    scrape_interval: 15s
    honor_labels: true
    metrics_path: '/federate'
    params:
      'match[]':
        - '{job="prometheus"}'
        - '{__name__=~"job:.*"}'
        - '{__name__=~"node.*"}'
    static_configs:
      - targets:
        - '192.168.77.11:9090'
        - '192.168.77.12:9090'
</code></pre> 
<p>为了有效的减少不必要的时间序列，通过params参数可以用于指定只获取某些时间序列的样本数据，例如</p> 
<p><code>"http://192.168.77.11:9090/federate?match[]={job%3D"prometheus"}&amp;match[]={__name__%3D~"job%3A.*"}&amp;match[]={__name__%3D~"node.*"}"</code></p> 
<p>通过URL中的match[]参数指定我们可以指定需要获取的时间序列。match[]参数必须是一个瞬时向量选择器，例如up或者{job=“api-server”}。配置多个match[]参数，用于获取多组时间序列的监控数据。</p> 
<p><strong>horbor_labels</strong>配置true可以确保当采集到的监控指标冲突时，能够自动忽略冲突的监控数据。如果为false时，prometheus会自动将冲突的标签替换为”exported_“的形式。</p> 
<h2><a id="_310"></a>功能分区</h2> 
<p>联邦集群的特性可以帮助用户根据不同的监控规模对Promthues部署架构进行调整。例如如下所示，可以在各个数据中心中部署多个Prometheus Server实例。每一个Prometheus Server实例只负责采集当前数据中心中的一部分任务(Job)，例如可以将不同的监控任务分离到不同的Prometheus实例当中，再有中心Prometheus实例进行聚合。</p> 
<p><img src="https://i-blog.csdnimg.cn/blog_migrate/b838e6a5ce9f45d9240b3e64d09220da.png#pic_center" alt="功能分区"></p> 
<p>功能分区，即通过联邦集群的特性在任务级别对Prometheus采集任务进行划分，以支持规模的扩展。</p> 
<h2><a id="Prometheus_321"></a><strong>Prometheus高可用</strong></h2> 
<p>Prometheus的本地存储给Prometheus带来了简单高效的使用体验，可以让Promthues在单节点的情况下满足大部分用户的监控需求。但是本地存储也同时限制了Prometheus的可扩展性，带来了数据持久化等一系列的问题。通过Prometheus的Remote Storage特性可以解决这一系列问题，包括Promthues的动态扩展，以及历史数据的存储。</p> 
<p>而除了数据持久化问题以外，影响Promthues性能表现的另外一个重要因素就是数据采集任务量，以及单台Promthues能够处理的时间序列数。因此当监控规模大到Promthues单台无法有效处理的情况下，可以选择利用Promthues的联邦集群的特性，将Promthues的监控任务划分到不同的实例当中。</p> 
<p>这一部分将重点讨论Prometheus的高可用架构，并且根据不同的使用场景介绍了一种常见的高可用方案。</p> 
<p><img src="https://i-blog.csdnimg.cn/blog_migrate/3325287de703c4f75e38e3dc9a252f6f.png#pic_center" alt="基本HA"></p> 
<p>基本的HA模式只能确保Promthues服务的可用性问题，但是不解决Prometheus Server之间的数据一致性问题以及持久化问题(数据丢失后无法恢复)，也无法进行动态的扩展。因此这种部署方式适合监控规模不大，Promthues Server也不会频繁发生迁移的情况，并且只需要保存短周期监控数据的场景。</p> 
<h3><a id="HA___338"></a>基本HA + 远程存储</h3> 
<p>在基本HA模式的基础上通过添加Remote Storage存储支持，将监控数据保存在第三方存储服务上。<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/ffcf306cd4d84473f329d30a48316cb1.png#pic_center" alt="HA + Remote Storage"></p> 
<p>在解决了Promthues服务可用性的基础上，同时确保了数据的持久化，当Promthues Server发生宕机或者数据丢失的情况下，可以快速的恢复。 同时Promthues Server可能很好的进行迁移。因此，该方案适用于用户监控规模不大，但是希望能够将监控数据持久化，同时能够确保Promthues Server的可迁移性的场景。</p> 
<h3><a id="HA_____345"></a>基本HA + 远程存储 + 联邦集群</h3> 
<p>当单台Promthues Server无法处理大量的采集任务时，用户可以考虑基于Prometheus联邦集群的方式将监控采集任务划分到不同的Promthues实例当中即在任务级别功能分区。<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/79c23efbccfb0d3f298cb70d1b564de4.png#pic_center" alt="基本HA + 远程存储 + 联邦集群"></p> 
<p>这种部署方式一般适用于两种场景：</p> 
<p>场景一：单数据中心 + 大量的采集任务</p> 
<p>这种场景下Promthues的性能瓶颈主要在于大量的采集任务，因此用户需要利用Prometheus联邦集群的特性，将不同类型的采集任务划分到不同的Promthues子服务中，从而实现功能分区。例如一个Promthues Server负责采集基础设施相关的监控指标，另外一个Prometheus Server负责采集应用监控指标。再有上层Prometheus Server实现对数据的汇聚。</p> 
<p>场景二：多数据中心</p> 
<p>这种模式也适合与多数据中心的情况，当Promthues Server无法直接与数据中心中的Exporter进行通讯时，在每一个数据中部署一个单独的Promthues Server负责当前数据中心的采集任务是一个不错的方式。这样可以避免用户进行大量的网络配置，只需要确保主Promthues Server实例能够与当前数据中心的Prometheus Server通讯即可。 中心Promthues Server负责实现对多数据中心数据的聚合。</p> 
<h3><a id="_363"></a>按照实例进行功能分区</h3> 
<p>这时在考虑另外一种极端情况，即单个采集任务的Target数也变得非常巨大。这时简单通过联邦集群进行功能分区，Prometheus Server也无法有效处理时。这种情况只能考虑继续在实例级别进行功能划分。<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/77efa63b32ca8c5456f7782fa32f5381.png#pic_center" alt="实例级别功能分区"></p> 
<p>如上图所示，将统一任务的不同实例的监控数据采集任务划分到不同的Prometheus实例。通过relabel设置，我们可以确保当前Prometheus Server只收集当前采集任务的一部分实例的监控指标。</p> 
<pre><code>global:
  external_labels:
    slave: 1  # This is the 2nd slave. This prevents clashes between slaves.
scrape_configs:
  - job_name: some_job
    relabel_configs:
    - source_labels: [__address__]
      modulus:       4
      target_label:  __tmp_hash
      action:        hashmod
    - source_labels: [__tmp_hash]
      regex:         ^1$
      action:        keep
</code></pre> 
<p>并且通过当前数据中心的一个中心Prometheus Server将监控数据进行聚合到任务级别。</p> 
<pre><code>- scrape_config:
  - job_name: slaves
    honor_labels: true
    metrics_path: /federate
    params:
      match[]:
        - '{__name__=~"^slave:.*"}'   # Request all slave-level time series
    static_configs:
      - targets:
        - slave0:9090
        - slave1:9090
        - slave3:9090
        - slave4:9090
</code></pre> 
<h3><a id="_404"></a>可用方案选择</h3> 
<p>上面的部分，根据不同的场景演示了3种不同的高可用部署方案。当然对于Promthues部署方案需要用户根据监控规模以及自身的需求进行动态调整，下表展示了Promthues和高可用有关3个选项各自解决的问题，用户可以根据自己的需求灵活选择</p> 
<table><thead><tr><th>选项\需求</th><th>服务可用性</th><th>数据持久化</th><th>水平扩展</th></tr></thead><tbody><tr><td>主备HA</td><td>v</td><td>x</td><td>x</td></tr><tr><td>远程存储</td><td>x</td><td>v</td><td>x</td></tr><tr><td>联邦集群</td><td>x</td><td>x</td><td>v</td></tr></tbody></table> 
<h2><a id="Alertmanager_416"></a><strong>Alertmanager高可用</strong></h2> 
<p>如下所示Alertmanager成为单点。</p> 
<p><img src="https://i-blog.csdnimg.cn/blog_migrate/66f1a2f5bca421af8db010ade765ece3.png#pic_center" alt="Alertmanager成为单点"><br> 为了提升Promthues的服务可用性，通常用户会部署两个或者两个以上的Promthus Server，它们具有完全相同的配置包括Job配置，以及告警配置等。当某一个Prometheus Server发生故障后可以确保Promthues持续可用。</p> 
<p>同时基于Alertmanager的告警分组机制即使不同的Prometheus Sever分别发送相同的告警给Alertmanager，Alertmanager也可以自动将这些告警合并为一个通知向receiver发送。<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/5eb91588a317619205938a3628b21c39.png#pic_center" alt="Alertmanager特性"><br> 但不幸的是，虽然Alertmanager能够同时处理多个相同的Prometheus Server所产生的告警。但是由于单个Alertmanager的存在，当前的部署结构存在明显的单点故障风险，当Alertmanager单点失效后，告警的后续所有业务全部失效。</p> 
<p>如下所示，最直接的方式，就是尝试部署多套Alertmanager。但是由于Alertmanager之间不存在并不了解彼此的存在，因此则会出现告警通知被不同的Alertmanager重复发送多次的问题。<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/7ff466e2e154c33b9862087fb48c4cb3.png#pic_center" alt="在这里插入图片描述"></p> 
<p>为了解决这一问题，如下所示。Alertmanager引入了Gossip机制。Gossip机制为多个Alertmanager之间提供了信息传递的机制。确保及时在多个Alertmanager分别接收到相同告警信息的情况下，也只有一个告警通知被发送给Receiver。</p> 
<p><img src="https://i-blog.csdnimg.cn/blog_migrate/4425da76aadef91cf43c95ef10b17fad.png#pic_center" alt="在这里插入图片描述"></p> 
<h3><a id="Gossip_438"></a>Gossip协议</h3> 
<p>Gossip是分布式系统中被广泛使用的协议，用于实现分布式节点之间的信息交换和状态同步。Gossip协议同步状态类似于流言或者病毒的传播，如下所示：</p> 
<p><img src="https://i-blog.csdnimg.cn/blog_migrate/23c7fd69f5ba2300df69c9eb6b2f2d83.png#pic_center" alt="在这里插入图片描述"></p> 
<p>一般来说Gossip有两种实现方式分别为Push-based和Pull-based。在Push-based当集群中某一节点A完成一个工作后，随机的从其它节点B并向其发送相应的消息，节点B接收到消息后在重复完成相同的工作，直到传播到集群中的所有节点。而Pull-based的实现中节点A会随机的向节点B发起询问是否有新的状态需要同步，如果有则返回。</p> 
<p>在简单了解了Gossip协议之后，我们来看Alertmanager是如何基于Gossip协议实现集群高可用的。如下所示，当Alertmanager接收到来自Prometheus的告警消息后，会按照以下流程对告警进行处理：<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/c00cbe25a1eab89bdc4cbfc3409c1364.png#pic_center" alt="在这里插入图片描述"></p> 
<p>通知流水线</p> 
<ol><li>在第一个阶段Silence中，Alertmanager会判断当前通知是否匹配到任何的静默规则，如果没有则进入下一个阶段，否则则中断流水线不发送通知。</li><li>在第二个阶段Wait中，Alertmanager会根据当前Alertmanager在集群中所在的顺序(index)等待index * 5s的时间。</li><li>当前Alertmanager等待阶段结束后，Dedup阶段则会判断当前Alertmanager数据库中该通知是否已经发送，如果已经发送则中断流水线，不发送告警，否则则进入下一阶段Send对外发送告警通知。</li><li>告警发送完成后该Alertmanager进入最后一个阶段Gossip，Gossip会通知其他Alertmanager实例当前告警已经发送。其他实例接收到Gossip消息后，则会在自己的数据库中保存该通知已发送的记录。</li></ol> 
<p>因此如下所示，Gossip机制的关键在于两点：<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/8a173dd5a754760634fe31ead6d7f10c.png#pic_center" alt="Gossip机制"></p> 
<ul><li>Silence设置同步：Alertmanager启动阶段基于Pull-based从集群其它节点同步Silence状态，当有新的Silence产生时使用Push-based方式在集群中传播Gossip信息。</li><li>通知发送状态同步：告警通知发送完成后，基于Push-based同步告警发送状态。Wait阶段可以确保集群状态一致。</li></ul> 
<p>Alertmanager基于Gossip实现的集群机制虽然不能保证所有实例上的数据时刻保持一致，但是实现了CAP理论中的AP系统，即可用性和分区容错性。同时对于Prometheus Server而言保持了配置了简单性，Promthues Server之间不需要任何的状态同步。</p> 
<h3><a id="_468"></a>搭建本地集群环境</h3> 
<p>为了能够让Alertmanager节点之间进行通讯，需要在Alertmanager启动时设置相应的参数。其中主要的参数包括：</p> 
<ul><li>–cluster.listen-address string: 当前实例集群服务监听地址</li><li>–cluster.peer value: 初始化时关联的其它实例的集群服务地址</li></ul> 
<p>例如：</p> 
<p>定义Alertmanager实例a1，其中Alertmanager的服务运行在9093端口，集群服务地址运行在8001端口。</p> 
<pre><code>alertmanager  --web.listen-address=":9093" --cluster.listen-address="127.0.0.1:8001" --config.file=/etc/prometheus/alertmanager.yml  --storage.path=/data/alertmanager/
</code></pre> 
<p>定义Alertmanager实例a2，其中主服务运行在9094端口，集群服务运行在8002端口。为了将a1，a2组成集群。 a2启动时需要定义–cluster.peer参数并且指向a1实例的集群服务地址:8001。</p> 
<pre><code>alertmanager  --web.listen-address=":9094" --cluster.listen-address="127.0.0.1:8002" --cluster.peer=127.0.0.1:8001 --config.file=/etc/prometheus/alertmanager.yml  --storage.path=/data/alertmanager2/
</code></pre> 
<p>为了能够在本地模拟集群环境，这里使用了一个轻量级的多线程管理工具goreman。使用以下命令可以在本地安装goreman命令行工具。</p> 
<p><code>go get github.com/mattn/goreman</code></p> 
<h3><a id="Alertmanager_493"></a>创建Alertmanager集群</h3> 
<p>创建Alertmanager配置文件/etc/prometheus/alertmanager-ha.yml, 为了验证Alertmanager的集群行为，这里在本地启动一个webhook服务用于打印Alertmanager发送的告警通知信息。</p> 
<pre><code>route:
  receiver: 'default-receiver'
receivers:
  - name: default-receiver
    webhook_configs:
    - url: 'http://127.0.0.1:5001/'
</code></pre> 
<p>本地webhook服务可以直接从Github获取。</p> 
<pre><code># 获取alertmanager提供的webhook示例，如果该目录下定义了main函数，go get会自动将其编译成可执行文件
go get github.com/prometheus/alertmanager/examples/webhook
# 设置环境变量指向GOPATH的bin目录
export PATH=$GOPATH/bin:$PATH
# 启动服务
webhook
</code></pre> 
<p>示例结构如下所示：<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/3bd4461dad4868733b22e97a6a035ca1.png#pic_center" alt="Alertmanager HA部署结构"></p> 
<p>创建alertmanager.procfile文件，并且定义了三个Alertmanager节点（a1，a2，a3）以及用于接收告警通知的webhook服务:</p> 
<pre><code>a1: alertmanager  --web.listen-address=":9093" --cluster.listen-address="127.0.0.1:8001" --config.file=/etc/prometheus/alertmanager-ha.yml  --storage.path=/data/alertmanager/ --log.level=debug
a2: alertmanager  --web.listen-address=":9094" --cluster.listen-address="127.0.0.1:8002" --cluster.peer=127.0.0.1:8001 --config.file=/etc/prometheus/alertmanager-ha.yml  --storage.path=/data/alertmanager2/ --log.level=debug
a3: alertmanager  --web.listen-address=":9095" --cluster.listen-address="127.0.0.1:8003" --cluster.peer=127.0.0.1:8001 --config.file=/etc/prometheus/alertmanager-ha.yml  --storage.path=/data/alertmanager2/ --log.level=debug

webhook: webhook
</code></pre> 
<p>在Procfile文件所在目录，执行goreman start命令，启动所有进程:</p> 
<pre><code>$ goreman -f alertmanager.procfile start
10:27:57      a1 | level=debug ts=2018-03-12T02:27:57.399166371Z caller=cluster.go:125 component=cluster msg="joined cluster" peers=0
10:27:57      a3 | level=info ts=2018-03-12T02:27:57.40004678Z caller=main.go:346 msg=Listening address=:9095
10:27:57      a1 | level=info ts=2018-03-12T02:27:57.400212246Z caller=main.go:271 msg="Loading configuration file" file=/etc/prometheus/alertmanager.yml
10:27:57      a1 | level=info ts=2018-03-12T02:27:57.405638714Z caller=main.go:346 msg=Listening address=:9093
</code></pre> 
<p>启动完成后访问任意Alertmanager节点http://localhost:9093/#/status,可以查看当前Alertmanager集群的状态。<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/f1c98169c741ce7374485dfa5972c22e.png#pic_center" alt="Alertmanager集群状态"></p> 
<p>当集群中的Alertmanager节点不在一台主机时，通常需要使用–cluster.advertise-address参数指定当前节点所在网络地址。</p> 
<blockquote> 
 <p>注意：由于goreman不保证进程之间的启动顺序，如果集群状态未达到预期，可以使用<code>goreman -f alertmanager.procfile run restart a2</code>重启a2，a3服务。</p> 
</blockquote> 
<p>当Alertmanager集群启动完成后，可以使用send-alerts.sh脚本对集群进行简单测试，这里利用curl分别向3个Alertmanager实例发送告警信息。</p> 
<pre><code>alerts1='[
  {
    "labels": {
       "alertname": "DiskRunningFull",
       "dev": "sda1",
       "instance": "example1"
     },
     "annotations": {
        "info": "The disk sda1 is running full",
        "summary": "please check the instance example1"
      }
  },
  {
    "labels": {
       "alertname": "DiskRunningFull",
       "dev": "sdb2",
       "instance": "example2"
     },
     "annotations": {
        "info": "The disk sdb2 is running full",
        "summary": "please check the instance example2"
      }
  },
  {
    "labels": {
       "alertname": "DiskRunningFull",
       "dev": "sda1",
       "instance": "example3",
       "severity": "critical"
     }
  },
  {
    "labels": {
       "alertname": "DiskRunningFull",
       "dev": "sda1",
       "instance": "example3",
       "severity": "warning"
     }
  }
]'

curl -XPOST -d"$alerts1" http://localhost:9093/api/v1/alerts
curl -XPOST -d"$alerts1" http://localhost:9094/api/v1/alerts
curl -XPOST -d"$alerts1" http://localhost:9095/api/v1/alerts
</code></pre> 
<p>运行send-alerts.sh后，查看alertmanager日志，可以看到以下输出，3个Alertmanager实例分别接收到模拟的告警信息：</p> 
<pre><code>10:43:36      a1 | level=debug ts=2018-03-12T02:43:36.853370185Z caller=dispatch.go:188 component=dispatcher msg="Received alert" alert=DiskRunningFull[6543bc1][active]
10:43:36      a2 | level=debug ts=2018-03-12T02:43:36.871180749Z caller=dispatch.go:188 component=dispatcher msg="Received alert" alert=DiskRunningFull[8320f0a][active]
10:43:36      a3 | level=debug ts=2018-03-12T02:43:36.894923811Z caller=dispatch.go:188 component=dispatcher msg="Received alert" alert=DiskRunningFull[8320f0a][active]
</code></pre> 
<p>看webhook日志只接收到一个告警通知：</p> 
<pre><code>10:44:06 webhook | 2018/03/12 10:44:06 {
10:44:06 webhook |  &gt;  "receiver": "default-receiver",
10:44:06 webhook |  &gt;  "status": "firing",
10:44:06 webhook |  &gt;  "alerts": [
10:44:06 webhook |  &gt;    {
10:44:06 webhook |  &gt;      "status": "firing",
10:44:06 webhook |  &gt;      "labels": {
10:44:06 webhook |  &gt;        "alertname": "DiskRunningFull",
</code></pre> 
<h3><a id="PrometheusAlertmanager_617"></a>多实例Prometheus与Alertmanager集群</h3> 
<p>由于Gossip机制的实现，在Promthues和Alertmanager实例之间不要使用任何的负载均衡，需要确保Promthues将告警发送到所有的Alertmanager实例中：</p> 
<pre><code>alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - 127.0.0.1:9093
      - 127.0.0.1:9094
      - 127.0.0.1:9095
</code></pre> 
<p>创建Promthues集群配置文件/etc/prometheus/prometheus-ha.yml，完整内容如下：</p> 
<pre><code>global:
  scrape_interval: 15s
  scrape_timeout: 10s
  evaluation_interval: 15s
rule_files:
  - /etc/prometheus/rules/*.rules
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - 127.0.0.1:9093
      - 127.0.0.1:9094
      - 127.0.0.1:9095
scrape_configs:
- job_name: prometheus
  static_configs:
  - targets:
    - localhost:9090
- job_name: 'node'
  static_configs:
  - targets: ['localhost:9100']
</code></pre> 
<p>同时定义告警规则文件/etc/prometheus/rules/hoststats-alert.rules，如下所示：</p> 
<pre><code>groups:
- name: hostStatsAlert
  rules:
  - alert: hostCpuUsageAlert
    expr: sum(avg without (cpu)(irate(node_cpu{mode!='idle'}[5m]))) by (instance) * 100 &gt; 50
    for: 1m
    labels:
      severity: page
    annotations:
      summary: "Instance {<!-- -->{ $labels.instance }} CPU usgae high"
      description: "{<!-- -->{ $labels.instance }} CPU usage above 50% (current value: {<!-- -->{ $value }})"
  - alert: hostMemUsageAlert
    expr: (node_memory_MemTotal - node_memory_MemAvailable)/node_memory_MemTotal * 100 &gt; 85
    for: 1m
    labels:
      severity: page
    annotations:
      summary: "Instance {<!-- -->{ $labels.instance }} MEM usgae high"
      description: "{<!-- -->{ $labels.instance }} MEM usage above 85% (current value: {<!-- -->{ $value }})"
</code></pre> 
<p>本示例部署结构如下所示：<br> <img src="https://i-blog.csdnimg.cn/blog_migrate/3bae4faaf5433ac0d1963cf8f5b15104.png#pic_center" alt="Promthues与Alertmanager HA部署结构"><br> 创建prometheus.procfile文件，创建两个Promthues节点，分别监听9090和9091端口</p> 
<pre><code>p1: prometheus --config.file=/etc/prometheus/prometheus-ha.yml --storage.tsdb.path=/data/prometheus/ --web.listen-address="127.0.0.1:9090"
p2: prometheus --config.file=/etc/prometheus/prometheus-ha.yml --storage.tsdb.path=/data/prometheus2/ --web.listen-address="127.0.0.1:9091"

node_exporter: node_exporter -web.listen-address="0.0.0.0:9100"
</code></pre> 
<p>使用goreman启动多节点Promthues：</p> 
<pre><code>goreman -f prometheus.procfile -p 8556 start
</code></pre> 
<p>Promthues启动完成后，手动拉高系统CPU使用率：</p> 
<pre><code>cat /dev/zero&gt;/dev/null
</code></pre> 
<blockquote> 
 <p>注意，对于多核主机，如果CPU达不到预期，运行多个命令。</p> 
</blockquote> 
<p>当CPU利用率达到告警规则触发条件，两个Prometheus实例告警分别被触发。查看Alertmanager输出日志：</p> 
<pre><code>11:14:41      a3 | level=debug ts=2018-03-12T03:14:41.945493505Z caller=dispatch.go:188 component=dispatcher msg="Received alert" alert=hostCpuUsageAlert[7d698ac][active]
11:14:41      a1 | level=debug ts=2018-03-12T03:14:41.945534548Z caller=dispatch.go:188 component=dispatcher msg="Received alert" alert=hostCpuUsageAlert[7d698ac][active]
11:14:41      a2 | level=debug ts=2018-03-12T03:14:41.945687812Z caller=dispatch.go:188 component=dispatcher msg="Received alert" alert=hostCpuUsageAlert[7d698ac][active]
</code></pre> 
<p>3个Alertmanager实例分别接收到来自不同Prometheus实例的告警信息。而Webhook服务只接收到来自Alertmanager集群的一条告警通知：</p> 
<pre><code>11:15:11 webhook | 2018/03/12 11:15:11 {
11:15:11 webhook |  &gt;  "receiver": "default-receiver",
11:15:11 webhook |  &gt;  "status": "firing",
11:15:11 webhook |  &gt;  "alerts": [
11:15:11 webhook |  &gt;    {
11:15:11 webhook |  &gt;      "status": "firing",
11:15:11 webhook |  &gt;      "labels": {
11:15:11 webhook |  &gt;        "alertname": "hostCpuUsageAlert",
</code></pre> 
<h2><a id="_729"></a>参考</h2> 
<p><a href="https://prometheus.io/docs/prometheus/latest/federation/" rel="nofollow">https://prometheus.io/docs/prometheus/latest/federation/</a></p>
                </div>
            </body>
            </html>
            